1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   // http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.modules;
14  
15  import org.apache.tapestry5.SymbolConstants;
16  import org.apache.tapestry5.internal.AssetConstants;
17  import org.apache.tapestry5.internal.InternalConstants;
18  import org.apache.tapestry5.internal.services.*;
19  import org.apache.tapestry5.internal.services.assets.*;
20  import org.apache.tapestry5.internal.services.messages.ClientLocalizationMessageResource;
21  import org.apache.tapestry5.ioc.*;
22  import org.apache.tapestry5.ioc.annotations.*;
23  import org.apache.tapestry5.ioc.services.FactoryDefaults;
24  import org.apache.tapestry5.ioc.services.SymbolProvider;
25  import org.apache.tapestry5.services.*;
26  import org.apache.tapestry5.services.assets.*;
27  import org.apache.tapestry5.services.javascript.JavaScriptStackSource;
28  import org.apache.tapestry5.services.messages.ComponentMessagesSource;
29  
30  import java.util.Map;
31  
32  /**
33   * @since 5.3
34   */
35  @Marker(Core.class)
36  public class AssetsModule
37  {
38      public static void bind(ServiceBinder binder)
39      {
40          binder.bind(AssetFactory.class, ClasspathAssetFactory.class).withSimpleId();
41          binder.bind(AssetPathConverter.class, IdentityAssetPathConverter.class);
42          binder.bind(AssetPathConstructor.class, AssetPathConstructorImpl.class);
43          binder.bind(ClasspathAssetAliasManager.class, ClasspathAssetAliasManagerImpl.class);
44          binder.bind(AssetSource.class, AssetSourceImpl.class);
45          binder.bind(StreamableResourceSource.class, StreamableResourceSourceImpl.class);
46          binder.bind(CompressionAnalyzer.class, CompressionAnalyzerImpl.class);
47          binder.bind(ContentTypeAnalyzer.class, ContentTypeAnalyzerImpl.class);
48          binder.bind(ResourceChangeTracker.class, ResourceChangeTrackerImpl.class);
49          binder.bind(ResourceMinimizer.class, MasterResourceMinimizer.class);
50          binder.bind(AssetChecksumGenerator.class, AssetChecksumGeneratorImpl.class);
51          binder.bind(JavaScriptStackAssembler.class, JavaScriptStackAssemblerImpl.class);
52      }
53  
54      @Contribute(AssetSource.class)
55      public void configureStandardAssetFactories(MappedConfiguration<String, AssetFactory> configuration,
56                                                  @ContextProvider
57                                                  AssetFactory contextAssetFactory,
58  
59                                                  @ClasspathProvider
60                                                  AssetFactory classpathAssetFactory)
61      {
62          configuration.add(AssetConstants.CONTEXT, contextAssetFactory);
63          configuration.add(AssetConstants.CLASSPATH, classpathAssetFactory);
64          configuration.add(AssetConstants.HTTP, new ExternalUrlAssetFactory(AssetConstants.HTTP));
65          configuration.add(AssetConstants.HTTPS, new ExternalUrlAssetFactory(AssetConstants.HTTPS));
66          configuration.add(AssetConstants.FTP, new ExternalUrlAssetFactory(AssetConstants.FTP));
67          configuration.add(AssetConstants.PROTOCOL_RELATIVE, new ExternalUrlAssetFactory(AssetConstants.PROTOCOL_RELATIVE));
68      }
69  
70  
71      @Contribute(SymbolProvider.class)
72      @FactoryDefaults
73      public static void setupSymbols(MappedConfiguration<String, Object> configuration)
74      {
75          // Minification may be enabled in production mode, but unless a minimizer is provided, nothing
76          // will change.
77          configuration.add(SymbolConstants.MINIFICATION_ENABLED, SymbolConstants.PRODUCTION_MODE_VALUE);
78          configuration.add(SymbolConstants.GZIP_COMPRESSION_ENABLED, true);
79          configuration.add(SymbolConstants.COMBINE_SCRIPTS, SymbolConstants.PRODUCTION_MODE_VALUE);
80          configuration.add(SymbolConstants.ASSET_URL_FULL_QUALIFIED, false);
81  
82          configuration.add(SymbolConstants.ASSET_PATH_PREFIX, "assets");
83  
84          configuration.add(SymbolConstants.BOOTSTRAP_ROOT, "${tapestry.asset.root}/bootstrap");
85  
86          configuration.add("tapestry.asset.root", "classpath:META-INF/assets/tapestry5");
87          configuration.add(SymbolConstants.OMIT_EXPIRATION_CACHE_CONTROL_HEADER, "max-age=60,must-revalidate");
88      }
89  
90      // The use of decorators is to allow third-parties to get their own extensions
91      // into the pipeline.
92  
93      @Decorate(id = "GZipCompression", serviceInterface = StreamableResourceSource.class)
94      public StreamableResourceSource enableCompression(StreamableResourceSource delegate,
95                                                        @Symbol(SymbolConstants.GZIP_COMPRESSION_ENABLED)
96                                                        boolean gzipEnabled, @Symbol(SymbolConstants.MIN_GZIP_SIZE)
97                                                        int compressionCutoff,
98                                                        AssetChecksumGenerator checksumGenerator)
99      {
100         return gzipEnabled
101                 ? new SRSCompressingInterceptor(delegate, compressionCutoff, checksumGenerator)
102                 : null;
103     }
104 
105     @Decorate(id = "CacheCompressed", serviceInterface = StreamableResourceSource.class)
106     @Order("before:GZIpCompression")
107     public StreamableResourceSource enableCompressedCaching(StreamableResourceSource delegate,
108                                                             @Symbol(SymbolConstants.GZIP_COMPRESSION_ENABLED)
109                                                             boolean gzipEnabled, ResourceChangeTracker tracker)
110     {
111         return gzipEnabled
112                 ? new SRSCompressedCachingInterceptor(delegate, tracker)
113                 : null;
114     }
115 
116     @Decorate(id = "Cache", serviceInterface = StreamableResourceSource.class)
117     @Order("after:GZipCompression")
118     public StreamableResourceSource enableUncompressedCaching(StreamableResourceSource delegate,
119                                                               ResourceChangeTracker tracker)
120     {
121         return new SRSCachingInterceptor(delegate, tracker);
122     }
123 
124     // Goes after cache, to ensure that what we are caching is the minified version.
125     @Decorate(id = "Minification", serviceInterface = StreamableResourceSource.class)
126     @Order("after:Cache,TextUTF8")
127     public StreamableResourceSource enableMinification(StreamableResourceSource delegate, ResourceMinimizer minimizer,
128                                                        @Symbol(SymbolConstants.MINIFICATION_ENABLED)
129                                                        boolean enabled)
130     {
131         return enabled
132                 ? new SRSMinimizingInterceptor(delegate, minimizer)
133                 : null;
134     }
135 
136     // Ordering this after minification means that the URL replacement happens first;
137     // then the minification, then the uncompressed caching, then compression, then compressed
138     // cache.
139     @Decorate(id = "CSSURLRewrite", serviceInterface = StreamableResourceSource.class)
140     @Order("after:Minification")
141     public StreamableResourceSource enableCSSURLRewriting(StreamableResourceSource delegate,
142                                                           OperationTracker tracker,
143                                                           AssetSource assetSource,
144                                                           AssetChecksumGenerator checksumGenerator,
145                                                           @Symbol(SymbolConstants.STRICT_CSS_URL_REWRITING) boolean strictCssUrlRewriting)
146     {
147         return new CSSURLRewriter(delegate, tracker, assetSource, checksumGenerator, strictCssUrlRewriting);
148     }
149 
150     @Decorate(id = "DisableMinificationForStacks", serviceInterface = StreamableResourceSource.class)
151     @Order("before:Minification")
152     public StreamableResourceSource setupDisableMinificationByJavaScriptStack(StreamableResourceSource delegate,
153                                                                               @Symbol(SymbolConstants.MINIFICATION_ENABLED)
154                                                                               boolean enabled,
155                                                                               JavaScriptStackSource javaScriptStackSource,
156                                                                               Request request)
157     {
158         return enabled
159                 ? new JavaScriptStackMinimizeDisabler(delegate, javaScriptStackSource, request)
160                 : null;
161     }
162 
163     /**
164      * Ensures that all "text/*" assets are given the UTF-8 charset.
165      *
166      * @since 5.4
167      */
168     @Decorate(id = "TextUTF8", serviceInterface = StreamableResourceSource.class)
169     @Order("after:Cache")
170     public StreamableResourceSource setupTextAssetsAsUTF8(StreamableResourceSource delegate)
171     {
172         return new UTF8ForTextAssets(delegate);
173     }
174 
175     /**
176      * Adds content types:
177      * <dl>
178      * <dt>css</dt>
179      * <dd>text/css</dd>
180      * <dt>js</dt>
181      * <dd>text/javascript</dd>
182      * <dt>jpg, jpeg</dt>
183      * <dd>image/jpeg</dd>
184      * <dt>gif</dt>
185      * <dd>image/gif</dd>
186      * <dt>png</dt>
187      * <dd>image/png</dd>
188      * <dt>svg</dt>
189      * <dd>image/svg+xml</dd>
190      * <dt>swf</dt>
191      * <dd>application/x-shockwave-flash</dd>
192      * <dt>woff</dt>
193      * <dd>application/font-woff</dd>
194      * <dt>tff</dt> <dd>application/x-font-ttf</dd>
195      * <dt>eot</dt> <dd>application/vnd.ms-fontobject</dd>
196      * </dl>
197      */
198     @Contribute(ContentTypeAnalyzer.class)
199     public void setupDefaultContentTypeMappings(MappedConfiguration<String, String> configuration)
200     {
201         configuration.add("css", "text/css");
202         configuration.add("js", "text/javascript");
203         configuration.add("gif", "image/gif");
204         configuration.add("jpg", "image/jpeg");
205         configuration.add("jpeg", "image/jpeg");
206         configuration.add("png", "image/png");
207         configuration.add("swf", "application/x-shockwave-flash");
208         configuration.add("svg", "image/svg+xml");
209         configuration.add("woff", "application/font-woff");
210         configuration.add("ttf", "application/x-font-ttf");
211         configuration.add("eot", "application/vnd.ms-fontobject");
212     }
213 
214     /**
215      * Disables compression for the following content types:
216      * <ul>
217      * <li>image/jpeg</li>
218      * <li>image/gif</li>
219      * <li>image/png</li>
220      * <li>image/svg+xml</li>
221      * <li>application/x-shockwave-flash</li>
222      * <li>application/font-woff</li>
223      * <li>application/x-font-ttf</li>
224      * <li>application/vnd.ms-fontobject</li>
225      * </ul>
226      */
227     @Contribute(CompressionAnalyzer.class)
228     public void disableCompressionForImageTypes(MappedConfiguration<String, Boolean> configuration)
229     {
230         configuration.add("image/*", false);
231         configuration.add("image/svg+xml", true);
232         configuration.add("application/x-shockwave-flash", false);
233         configuration.add("application/font-woff", false);
234         configuration.add("application/x-font-ttf", false);
235         configuration.add("application/vnd.ms-fontobject", false);
236     }
237 
238     @Marker(ContextProvider.class)
239     public static AssetFactory buildContextAssetFactory(ApplicationGlobals globals,
240                                                         AssetPathConstructor assetPathConstructor,
241                                                         ResponseCompressionAnalyzer compressionAnalyzer,
242                                                         ResourceChangeTracker resourceChangeTracker,
243                                                         StreamableResourceSource streamableResourceSource)
244     {
245         return new ContextAssetFactory(compressionAnalyzer, resourceChangeTracker, streamableResourceSource, assetPathConstructor, globals.getContext());
246     }
247 
248     @Contribute(ClasspathAssetAliasManager.class)
249     public static void addApplicationAndTapestryMappings(MappedConfiguration<String, String> configuration,
250 
251                                                          @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM)
252                                                          String appPackage)
253     {
254         configuration.add("tapestry", "org/apache/tapestry5");
255 
256         configuration.add("app", toPackagePath(appPackage));
257     }
258 
259     /**
260      * Contributes an handler for each mapped classpath alias, as well handlers for context assets
261      * and stack assets (combined {@link org.apache.tapestry5.services.javascript.JavaScriptStack} files).
262      */
263     @Contribute(Dispatcher.class)
264     @AssetRequestDispatcher
265     public static void provideBuiltinAssetDispatchers(MappedConfiguration<String, AssetRequestHandler> configuration,
266 
267                                                       @ContextProvider
268                                                       AssetFactory contextAssetFactory,
269 
270                                                       @Autobuild
271                                                       StackAssetRequestHandler stackAssetRequestHandler,
272 
273                                                       ClasspathAssetAliasManager classpathAssetAliasManager,
274                                                       ResourceStreamer streamer,
275                                                       AssetSource assetSource)
276     {
277         Map<String, String> mappings = classpathAssetAliasManager.getMappings();
278 
279         for (String folder : mappings.keySet())
280         {
281             String path = mappings.get(folder);
282 
283             configuration.add(folder, new ClasspathAssetRequestHandler(streamer, assetSource, path));
284         }
285 
286         configuration.add(RequestConstants.CONTEXT_FOLDER,
287                 new ContextAssetRequestHandler(streamer, contextAssetFactory.getRootResource()));
288 
289         configuration.add(RequestConstants.STACK_FOLDER, stackAssetRequestHandler);
290 
291     }
292 
293     @Contribute(ClasspathAssetAliasManager.class)
294     public static void addMappingsForLibraryVirtualFolders(MappedConfiguration<String, String> configuration,
295                                                            ComponentClassResolver resolver)
296     {
297         // Each library gets a mapping or its folder automatically
298 
299         Map<String, String> folderToPackageMapping = resolver.getFolderToPackageMapping();
300 
301         for (String folder : folderToPackageMapping.keySet())
302         {
303             // This is the 5.3 version, which is still supported:
304             configuration.add(folder, toPackagePath(folderToPackageMapping.get(folder)));
305 
306             // This is the 5.4 version; once 5.3 support is dropped, this can be simplified, and the
307             // "meta/" prefix stripped out.
308             String folderSuffix = folder.equals("") ? folder : "/" + folder;
309 
310             configuration.add("meta" + folderSuffix, "META-INF/assets" + folderSuffix);
311         }
312     }
313 
314     private static String toPackagePath(String packageName)
315     {
316         return packageName.replace('.', '/');
317     }
318 
319     /**
320      * Contributes:
321      * <dl>
322      * <dt>ClientLocalization</dt>
323      * <dd>A virtual resource of formatting symbols for decimal numbers</dd>
324      * <dt>Core</dt>
325      * <dd>Built in messages used by Tapestry's default validators and components</dd>
326      * <dt>AppCatalog</dt>
327      * <dd>The Resource defined by {@link SymbolConstants#APPLICATION_CATALOG}</dd>
328      * <dt>
329      *     </dl>
330      *
331      * @since 5.2.0
332      */
333     @Contribute(ComponentMessagesSource.class)
334     public static void setupGlobalMessageCatalog(AssetSource assetSource,
335                                                  @Symbol(SymbolConstants.APPLICATION_CATALOG)
336                                                  Resource applicationCatalog, OrderedConfiguration<Resource> configuration)
337     {
338         configuration.add("ClientLocalization", new ClientLocalizationMessageResource());
339         configuration.add("Core", assetSource.resourceForPath("org/apache/tapestry5/core.properties"));
340         configuration.add("AppCatalog", applicationCatalog);
341     }
342 
343     @Contribute(Dispatcher.class)
344     @Primary
345     public static void setupAssetDispatch(OrderedConfiguration<Dispatcher> configuration,
346                                           @AssetRequestDispatcher
347                                           Dispatcher assetDispatcher)
348     {
349 
350         // This goes first because an asset to be streamed may have an file
351         // extension, such as
352         // ".html", that will confuse the later dispatchers.
353 
354         configuration.add("Asset", assetDispatcher, "before:ComponentEvent");
355     }
356 }